/**
as3app - An open-source ActionScript 3.0 Library developed to assist 
in creating simple visual controls/data binding/asset loading for the
Flash player platform.

http://code.google.com/p/as3app/

Copyright (c) 2011 M. K. Chowdhury, All Rights Reserved.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package as3app.appLoader 
{
	import as3app.appLoader.Assets.Asset;
	import as3app.core.debug.IDump;
	import as3app.core.debug.ILog;
	import flash.display.Loader;
	import flash.utils.getDefinitionByName;
	import as3app.core.debug.logging.LogFactory;
	
	
	/**
	 * ...
	 * @author Muntasir Kabir Chowdhury
	 */
	public class AppLoader
	{
		private static var _instance:AppLoader = null;
		private var logger:ILog;
		
		public static const STATUS_IN_PROGRESS:int = 0;
		public static const STATUS_EMPTY:int = 1;
		public static const STATUS_PAUSED:int = 2;
		public static const STATUS_AVAILABLE:int = 3;
		public static const STATUS_ERROR:int = 4;
		
		public function AppLoader(singelton:AppLoaderSingletoneBlocker) 
		{
			logger = LogFactory.Register(this);
			
			logger.Temp("AppLoader() Initializing...");
			
			if (singelton != null && typeof(singelton) == typeof(AppLoaderSingletoneBlocker))
			{
				// Initialize component.
				//Asset.CallBack = this.On_AssetStatusChanged;
				_batches[0] = new Array();
				
				logger.Clear();
				logger.Log("AppLoader Initialized.");
				return;
			}
			
			logger.Commit();
			logger.Log("Error : Abstract Class Called : AppLoader()" );
			AppLoader._instance = null;
			throw("com.as3app.as3appLoader.AppLoader | Error : Can not initialize abstract class 'com.as3app.as3appLoader.AppLoader'");
			
		}
		
		
		
		
		protected var _assets:Array = new Array(), _dictionary:Array = new Array(), _batches:Array = new Array(), _batchCallbacks:Array = new Array();
		protected var _inProgress:Boolean = false, _preFetchCount:int = 1;
		protected var _preFetching:int = 0, _batchDownloading:int = 0;
		protected var _totalDownloaded:int = 0, _totalError:int = 0;
		
		
		
		
		/**
		 * Checks batch status.
		 * @param	batchID
		 */
		protected function CheckBatch(batchID:int)
		{
			if (_batches[batchID].Finished) return;
			
			
			var batch:Array = _batches[batchID].Keys;
			for each(var key:String in batch)
			{
				if ((_dictionary[key] as Asset).Status == Asset.STATUS_IN_PROGRESS) return;
			}
			
			_batches[batchID].Finished = true;
			if (_batchCallbacks[batchID] != null)
				(_batchCallbacks[batchID] as Function).apply(null, [BatchStatus(batchID)]);
				
				
			DoPrefetch();
			
		}
		
		
		
		
		/**
		 * Implements BatchAssetHandling portion
		 * @param	asset
		 */
		protected function DoBatchAssetStatusHandler(asset:Asset)
		{
			switch(asset.Status)
			{
				case Asset.STATUS_AVAILABLE:
					
					--_batchDownloading;
					var batchIDs:Array = asset.BatchIDs;
					for each(var batchID:int in batchIDs) CheckBatch(batchID);
					break;
					
				case Asset.STATUS_EMPTY:
					asset.Start();
					break;
					
				case Asset.STATUS_ERROR:
					--_batchDownloading;
					for each(var batchID2:int in batchIDs) CheckBatch(batchID2);
					break;
			}
		}
		
		
		
		
		
		/**
		 * Implements asset status handling portion
		 * @param	asset
		 * @param	prevStatus
		 */
		protected function DoAssetStatusHandler(asset:Asset,prevStatus:int)
		{
			//logger.Log("Asset Handler. Key=" + asset.Key + "  Status=" + asset.Status + "  Prev Status=" + prevStatus);
			
			switch(asset.Status)
			{
				case Asset.STATUS_AVAILABLE:
					_totalDownloaded++;
					
					if (asset.BatchIDs.length > 0)
					{
						
						--_batchDownloading;
						var batchIDs1:Array = asset.BatchIDs;
						for each(var batchID1:int in batchIDs1) CheckBatch(batchID1);
					}
					else
					{
						--_preFetching;
						
						DoPrefetch();
					}
					break;
					
					
					
				case Asset.STATUS_EMPTY:
			
					if (asset.BatchIDs.length > 0)
					{
						if (prevStatus == Asset.STATUS_IN_PROGRESS) --_batchDownloading;
						var batchIDs2:Array = asset.BatchIDs;
						for each(var batchID2:int in batchIDs2) CheckBatch(batchID2);
					}
					else
					{
						if (prevStatus == Asset.STATUS_IN_PROGRESS) --_preFetching;
						DoPrefetch();
					}
					break;
					
					
					
				case Asset.STATUS_ERROR:
				
					++_totalError;
					if (asset.BatchIDs.length > 0)
					{
						if (prevStatus == Asset.STATUS_IN_PROGRESS) --_batchDownloading;
						var batchIDs:Array = asset.BatchIDs;
						for each(var batchID:int in batchIDs) CheckBatch(batchID);
					}
					else
					{
						if (prevStatus == Asset.STATUS_IN_PROGRESS) --_preFetching;
						DoPrefetch();
					}
					break;
					
					
					
					
				case Asset.STATUS_IN_PROGRESS:
				
					break;
					
					
					
				case Asset.STATUS_PAUSED:
					if (asset.BatchIDs.length > 0)
					{
						if (prevStatus == Asset.STATUS_IN_PROGRESS) --_batchDownloading;
						var batchIDs3:Array = asset.BatchIDs;
						for each(var batchID3:int in batchIDs3) CheckBatch(batchID3);
					}
					else
					{
						if (prevStatus == Asset.STATUS_IN_PROGRESS) --_preFetching;
						DoPrefetch();
					}
					break;
		
					
			}
		}
		
		
		
		protected function DoPrefetch():void
		{
			while (_inProgress && _batchDownloading == 0 && _preFetching < _preFetchCount && TotalRemaining>_preFetching)
			{
		
				/*
				logger.Log("Prefetching. In Loop :: " + String(_inProgress) +
				" &&  "+_batchDownloading + "==0" +
				" &&  " + _preFetching +
				"<" + _preFetchCount +
				" &&  " + TotalRemaining.toString() +
				">" + _preFetching.toString());
				*/
				for (var i:int = _assets.length - 1; i >= 0; --i)
				{
					var asset:Asset = _dictionary[_assets[i].id as String] as Asset;
					if (asset.Status == Asset.STATUS_EMPTY || asset.Status == Asset.STATUS_PAUSED)
					{
						asset.Start();
						if (asset.Status == Asset.STATUS_IN_PROGRESS) _preFetching++;
						break;
					}
				}
			}
			
			
			/*
			logger.Log("Prefetching. Outof Loop :: " + String(_inProgress) +
				" &&  "+_batchDownloading + "==0" +
				" &&  " + _preFetching +
				"<" + _preFetchCount +
				" &&  " + TotalRemaining.toString() +
				">" + _preFetching.toString());
			*/
		}
		
		
		
		
		
		
		/**
		 * Implements Add portion.
		 * @param	callback
		 * @param	keyList
		 * @return
		 */
		protected function DoAdd(key:String, priority:int, url:String=null):void
		{
			logger.Temp("Adding  key=" + String(key) + "  priority=" + priority.toString() + "  url=" + String(url));
			
			if (url == null) url = key;
			
			if (_dictionary[key] != null) 
			{
				logger.Commit();
				logger.Log("Error : Asset with a same key already exists.");
				throw("com.as3app.as3appLoader.AppLoader::Add | Error : Can not add asset. An asset already exists with same key. Key:'" + key + "'");
				return;
			}
			else 
			{
				var asset:Asset = new Asset(key, url, priority);
				asset.CallBack = DoAssetStatusHandler;
				
				_dictionary[key] = asset;
				
				var object:Object = new Object();
				
				object.value = priority.toString();
				object.id = key;
				_assets.push(object);
				_assets.sortOn("value", Array.NUMERIC);
				
				if (_inProgress) DoPrefetch();
				
			}
			
			logger.Clear();
		}
		
		
		
		
		
		
		/**
		 * Implements required portion.
		 * @param	callback
		 * @param	keyList
		 * @return
		 */
		protected function DoRequired(callback:Function,keyList:Array):int
		{
			logger.Temp("Required() callback=" + String(callback) + "  keyList=" + String(keyList));
			
			var totalDownloadedYet:int = 0;
			var totalErrorOccured:int = 0;
			for each(var key11:String in keyList) 
			{
				if (_dictionary[key11] == null)
				{
					logger.Commit();
					logger.Log("Required() Error:No key existe with value" + key11);
					throw("as3app.as3appLoader.AppLoader::Required() :: Error:No key existe with value" + key11);
				}
				if ((_dictionary[key11] as Asset).Status == Asset.STATUS_AVAILABLE) ++totalDownloadedYet;
				else if ((_dictionary[key11] as Asset).Status == Asset.STATUS_ERROR) ++totalErrorOccured;
			}
			
			if (totalDownloadedYet+totalErrorOccured == keyList.length)
			{
				var status:DownloadStatus = new DownloadStatus();
				status.Total = keyList.length;
				status.Finished = totalDownloadedYet;
				status.Error = totalErrorOccured;
				status.Remaining = status.Total - totalDownloadedYet - totalErrorOccured;
				status.Keys = keyList;
				status.BatchID = 0;
				
				if (callback != null) callback.apply(null, [status]);
				
				return 0;
			}
			
			var batchID:int = _batches.length;
			_batches[batchID] = { Finished:false, Keys:keyList };
			_batchCallbacks[batchID] = callback;
			var array:Array = keyList;
			
			logger.Temp("Required() Created batchID=" + batchID.toString());
			
			try
			{
				for each(var key:String in keyList)
				{
					logger.Temp("Required() batchID=" + batchID.toString() + "  Inserting asset with key=" + key);
					var asset:Asset = _dictionary[key] as Asset;
					
						
					
					
						
					logger.Temp("Required()  Asset is added to batchID.");
					if (asset.Status == Asset.STATUS_EMPTY || asset.Status == Asset.STATUS_PAUSED) 
					{
						logger.Temp("Required() Starting asset download....");
						
						_batchDownloading++;
						
						asset.AddToBatchID(batchID);
						asset.Start();
					}
					else if (asset.Status == Asset.STATUS_IN_PROGRESS)
					{
						logger.Temp("Required() Asset already in progress.");	
						if (asset.BatchIDs.length == 0)--_preFetching;

						asset.AddToBatchID(batchID);
					}
					logger.Temp("Required() Saved batchID=" + batchID.toString() + "  done. Asset key=" + key);
					
					
				}
				logger.Clear();
				logger.Log("Required() Created new Batch. batchID=" + batchID.toString() + "  Success.");
			}
			catch (error:Error)
			{
				logger.Commit();
				logger.Log("Exception.  ## " + error.getStackTrace());
				throw("as3app.as3appLoader.AppLoader::Required() :: Error: Unexpected. Check Log");
				
			}
			
			return batchID;
			
		}
		
		
		
		
		
		
		
		
		/**
		 * Hetre will be all non static functions
		 */
		
		
		
		
		
		
		
		/*
		 * #region Static Public function's
		 * Here will be all Static Public Function function's
		 * 
		 * 
		 */
		
		
		 
		/**
		 * Init AppLoader
		 */
		public static function Init():void
		{
			if (_instance == null) _instance = new AppLoader(new AppLoaderSingletoneBlocker());
		}
		
		
		
		
		/**
		 * Adds asset for downloading
		 * @param	key
		 * @param	priority
		 * @param	url
		 */
		public static function Add(key:String, priority:int, url:String)
		{
			if (AppLoader._instance == null) AppLoader.Init();
			_instance.DoAdd(key, priority, url);
		}
		
		
		
		
		
		
		
		/**
		 * Determines wether the given keys already exists or not.
		 * @param	keys		List of AppLoader asset key(s). 
		 * @return	Boolean. Has key or not.
		 */
		public static function HasKey(... keys):Boolean
		{
			for each(var key:String in keys) if (_instance._dictionary[key]==null) return false;
			return true;
		}
		
		
		
		
		
		
		/**
		 * Determines wether the given assets of the given keys already available or not.
		 * @param	... keys
		 * @return		Boolean. 
		 */
		public static function IsAvailable(... keys):Boolean
		{
			for each(var key:String in keys) if ((_instance._dictionary[key] as Asset).Status != Asset.STATUS_AVAILABLE) return false;
			return true;
		}
		
		
		
		
		
		
		/**
		 * Determines wether the batch downloading with the given batchID id already exists or not.
		 * @param	batchDownloadID		int
		 * @return		Boolean
		 */
		public static function HasBatch(batchDownloadID:int):Boolean
		{
			return (_instance._batches[batchDownloadID] != null);
		}
		
		
		
		
		
		
		/**
		 * Get batch downloading status of the given batchID.
		 * @param	batchDownloadID
		 * @throws Exception if batchID doesnt exists. Check whether batchID exists
		 * by calling AppLoader.HasBatch(batchID:int):Boolean
		 * 
		 * @return	batchDownLoad status=new Object()
		 */
		public static function BatchStatus(batchDownloadID:int):DownloadStatus
		{
			var status:DownloadStatus = new DownloadStatus();
			var batch:Array = _instance._batches[batchDownloadID].Keys as Array;
			status.Total = batch.length;
			status.BatchID = batchDownloadID;
			
			for each(var key:String in batch)
			{
				var assetStatus:DownloadStatus = AppLoader.AssetStatus(key);
				status.Error += assetStatus.Error;
				status.Finished += assetStatus.Finished;
				status.Remaining += assetStatus.Remaining;
				if (assetStatus.Error == 0)
				{
					status.TotalBytes += assetStatus.TotalBytes;
					status.DownloadedBytes += assetStatus.DownloadedBytes;
				}
				
				status.Assets[key] = assetStatus;
				status.Keys.push(key);
				
			}
			
			
			return status;
		}
		
		
		
		
		
		
		/**
		 * Asset status for the given key.
		 * @param	key
		 * @return DownloadStatus :: new Object
		 * 
		 * @throws Exception if key doesnt exists. Check whether the key really 
		 * exists by calling HasKey(key:String):Boolean
		 */
		public static function AssetStatus(key:String):DownloadStatus
		{
			var status:DownloadStatus = new DownloadStatus();
			var asset:Asset = _instance._dictionary[key];
			status.Keys.push(key);
			status.BatchID = -1;
			
			status.TotalBytes = asset.contentLoaderInfo.bytesTotal;
			status.DownloadedBytes = asset.contentLoaderInfo.bytesLoaded;
			if (asset.Status == Asset.STATUS_AVAILABLE) { status.Finished = 1; status.Remaining = 0; }
			else if (asset.Status == Asset.STATUS_ERROR) { status.Error = 1; status.Remaining = 0;}
			
			return status;
		}
		
		
		
		
		
		
		/**
		 * Get or set the total number of concurrent prefetching.
		 */
		public static function get PrefetchCount():int { return _instance._preFetchCount; }
		public static function set PrefetchCount(preFetch:int) { _instance._preFetchCount = preFetch; }
		
		
		
		
		
		/**
		 * Get total number of completed download throughout the lifetime of the application.
		 */
		public static function get TotalDownloaded():int { return _instance._totalDownloaded; }
		
		
		
		
		
		
		/**
		 * Get total number of download throughout the lifetime of the application.
		 */
		public static function get TotalDownload():int { return _instance._assets.length; }
		
		
		
		
		
		
		/**
		 * Get total number of remaining download throughout the lifetime of the application.
		 */
		public static function get TotalRemaining():int { return _instance._assets.length - _instance._totalDownloaded - _instance._totalError; }
		
		
		
		
		
		
		
		/**
		 * Get total error occured throughout the lifetime of the application.
		 */
		public static function get TotalError():int { return _instance._totalError; }
		
		
		
		
		
		
		
		/**
		 * Starts pre fetching all the added assets by pririty.
		 */
		public static function get PreFetch():Boolean 
		{ 
			return _instance._inProgress; 
		}
		public static function set PreFetch(preFetch:Boolean) 
		{ 
			_instance._inProgress = preFetch;
			_instance.DoPrefetch();
		}
		
		
		
		
		
		
		/**
		 * Initiates batch download. resumes all prefetching and start downloading
		 * the given asset keys ignoring all priorities. 
		 * @param	callback	Callback when done. function(status:DownloadStatus):void
		 * @param	...keys		Asset keys.
		 * @return	int : BatchID of the created batch.
		 */
		public static function Required(callback:Function, ...keys):int
		{
			return _instance.DoRequired(callback, keys);
		}
		
		
		
		
		/**
		 * Initiates batch download. resumes all prefetching and start downloading
		 * the given asset keys ignoring all priorities. 
		 * @param	callback	Callback when done. function(status:DownloadStatus):void
		 * @param	...keys		Asset keys.
		 * @return	int : BatchID of the created batch.
		 */
		public static function RequiredInArray(callback:Function, keys:Array):int
		{
			return _instance.DoRequired(callback, keys);
		}
		
		
		
		
		
		/**
		 * @see
		 * for debugging and test purpos only.
		 * @return
		 */
		public static function Show():String
		{
			var out:String = "Assets :  \n";
			/*
			for (var i:int = 0; i < _instance._assets.length;++i )
			{
				out += i.toString() + "  :  " + _instance._assets[i].id + " | " + String(_instance._assets[i].value) + "\n";
			}
			*/
			out = "%APPLOADER%:: Total = " + TotalDownload + ", Downloaded = " + TotalDownloaded + ", Error = " + TotalError + ", Remaining = " + TotalRemaining +
				", Batch DL :" + AppLoader._instance._batchDownloading+", Prefetch DL :" + AppLoader._instance._preFetching;
			return out;
		}
		
		
		
		
		
		
		
		/**
		 * Set priority of an Asset.
		 * @param	key		Key of the asset.
		 * @param	priority	New priority of the asset.
		 * @throws 	Exception if Asset with a given key doesnt exists.
		 * @see	Slows down, have to loop all assets.
		 */
		public static function SetPriority(key:String, priority:int)
		{
			if (priority > 0)
			{
				(_instance._dictionary[key] as Asset).Priority = priority;
				
				for each(var o:Object in _instance._assets)
				{
					if (o.id == key)
					{
						o.value = priority;
						_instance._assets.sortOn("value", Array.NUMERIC);
						return;
					}
				}
			}
		}
		
		
		
		
		
		
		/**
		 * Get priority of Asset.
		 * @param	key		Key of the asset.
		 * @return 	Priority of the asset in Int.
		 * @throws 	Exception if Asset with a given key doesnt exists.
		 */
		public static function GetPriority(key:String):int
		{
			return (_instance._dictionary[key] as Asset).Priority;
		}
		
		
		
		
		
		
		
		/**
		 * Gets count of total number in given AppLoader.STATUS_* that already included in AppLoader using Include() function
		 * @param	status	Asset Status :: AppLoader.STATUS_*
		 * @return	int. Total count of assets of provided status. 
		 */
		public static function GroupStatus(status:int):int
		{
			var count:int = 0;
			for each(var asset:Asset in _instance._dictionary)
			{
				if (asset.Status == status) ++count;
			}
			return count;
		}
		
		
		
		
		
		
		/**
		 * Get all keys in given AppLoader.STATUS_* that already included in AppLoader using Include() function
		 * @param	status Asset Status :: AppLoader.STATUS_*
		 * @return Array : Array of keys
		 */
		public static function GetKeys(status:int):Array
		{
			var keys:Array = new Array();
			for each(var asset:Asset in _instance._dictionary)
			{
				if (asset.Status == status) keys.push(asset.Key);
			}
			return keys;
		}
		
		
		/**
		 * Returns asset from the given key.
		 * @param	key	Key of the asset.
		 * @return Loader
		 * @see		returns null if no key exists.
		 */
		public static function Get(key:String):Loader { return _instance._dictionary[key] as Loader; } 

	}
}